/*
 * Copyright (c) 2002 - 2014 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */

/*
*  Copyright (c) 2013,
*      Tobias Blaschke <code@tobiasblaschke.de>
*  All rights reserved.

*  Redistribution and use in source and binary forms, with or without
*  modification, are permitted provided that the following conditions are met:
*
*  1. Redistributions of source code must retain the above copyright notice,
*     this list of conditions and the following disclaimer.
*
*  2. Redistributions in binary form must reproduce the above copyright notice,
*     this list of conditions and the following disclaimer in the documentation
*     and/or other materials provided with the distribution.
*
*  3. The names of the contributors may not be used to endorse or promote
*     products derived from this software without specific prior written
*     permission.
*
*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
*  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
*  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
*  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
*  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
*  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
*  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
*  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
*  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
*  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
*  POSSIBILITY OF SUCH DAMAGE.
*/
package com.ibm.wala.ipa.summaries;

import com.ibm.wala.core.util.strings.Atom;
import com.ibm.wala.ssa.ConstantValue;
import com.ibm.wala.ssa.SSAGotoInstruction;
import com.ibm.wala.ssa.SSAInstruction;
import com.ibm.wala.ssa.SSAInstructionFactory;
import com.ibm.wala.ssa.SymbolTable;
import com.ibm.wala.types.MemberReference;
import com.ibm.wala.types.TypeReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Instructions can be added in a non-ascending manner.
 *
 * <p>The later position of the instruction is determined by it's iindex. Additionally this Summary
 * may be instructed to prune unnecessary instructions.
 *
 * <p>However don't go berserk with the iindex as this will consume loads of memory.
 *
 * <p>You can get an ordinary MethodSummary using the {@link #getMethodSummary()}-Method.
 *
 * <p>It extends the MethodSummaries capabilities by the functions: * {@link #getStatementAt(int)} *
 * {@link #reserveProgramCounters(int)} * {@link #allowReserved(boolean)}
 *
 * @see com.ibm.wala.ssa.SSAInstructionFactory
 * @see com.ibm.wala.ipa.callgraph.impl.FakeRootMethod
 * @see com.ibm.wala.ipa.summaries.MethodSummary
 * @author Tobias Blaschke &lt;code@tobiasblaschke.de&gt;
 * @since 2013-09-08
 */
public class VolatileMethodSummary {

  private static final boolean DEBUG = false;
  private boolean allowReservedPC = false;
  private final MethodSummary summary;
  private List<SSAInstruction> instructions = new ArrayList<>();
  private final Map<Integer, Atom> localNames = new HashMap<>();
  private int currentProgramCounter = 0;
  private boolean locked = false;

  /** Placeholder for Reserved slots. */
  private static final class Reserved extends SSAInstruction {
    public Reserved() {
      super(SSAInstruction.NO_INDEX);
    }

    @Override
    public SSAInstruction copyForSSA(SSAInstructionFactory insts, int[] defs, int[] uses) {
      throw new IllegalStateException();
    }

    @Override
    public int hashCode() {
      return 12384;
    }

    @Override
    public boolean isFallThrough() {
      return true;
    }

    @Override
    public String toString(SymbolTable symbolTable) {
      return "Reserved Slot";
    }

    @Override
    public void visit(IVisitor v) {
      throw new IllegalStateException();
    }
  }

  private static final Reserved RESERVED = new Reserved();

  /**
   * @param summary a "real" summary methods get added to.
   * @throws IllegalArgumentException if this summary is null or not empty
   */
  public VolatileMethodSummary(MethodSummary summary) {
    if (summary == null) {
      throw new IllegalArgumentException("The given summary is null");
    }
    if (summary.getNumberOfStatements() > 0) {
      throw new IllegalArgumentException("The given summary is not empty");
    }
    this.summary = summary;
  }

  /**
   * @param programCounter the ProgramCounter to retrieve the Instruction from
   * @return The instruction or null if there is none
   * @throws IllegalArgumentException if the ProgramCounter is negative
   */
  public SSAInstruction getStatementAt(int programCounter) {
    if (programCounter < 0) {
      throw new IllegalArgumentException("Program-Counter may not be negative!");
    }
    if (this.instructions.size() <= programCounter) {
      return null;
    }
    if (this.instructions.get(programCounter).equals(RESERVED)) {
      return null;
    }
    return this.instructions.get(programCounter);
  }

  /**
   * Reserves an amount of ProgramCounters for later use.
   *
   * <p>This method reserves a count of ProgramCounters and thus affects the value returned by
   * getNextProgramCounter. It also marks these ProgramCounters as reserved so you can't use them
   * unless you explicitly allow it by {@link #allowReserved(boolean)}.
   *
   * @param count The amount of ProgramCounters to reserve ongoing from the current ProgramCounter
   * @throws IllegalArgumentException if the count is negative (a count of zero is however ok)
   */
  public void reserveProgramCounters(int count) {
    if (count < 0) {
      throw new IllegalArgumentException(
          "The count of ProgramCounters to reserve may not be negative");
    }
    for (int i = 0; i < count; ++i) {
      instructions.add(RESERVED);
    }
    currentProgramCounter += count;
  }

  /**
   * (Dis-)allows the usage of reserved ProgramCounters.
   *
   * <p>The setting of this function defaults to disallow upon class creation
   *
   * @param enable A value of true allows the usage of all reserved ProgramCounters
   * @return the previous value of allowReserved
   */
  public boolean allowReserved(boolean enable) {
    boolean prev = this.allowReservedPC;
    this.allowReservedPC = enable;
    return prev;
  }

  /**
   * Returns if the ProgramCounter is reserved.
   *
   * @param programCounter the ProgramCounter in question
   * @return true if the position is reserved
   * @throws IllegalArgumentException if the ProgramCounter is negative
   */
  public boolean isReserved(int programCounter) {
    if (programCounter < 0) {
      throw new IllegalArgumentException("The Program-Counter may not be negative");
    }

    if (instructions.size() - 1 < programCounter) return false;
    if (instructions.get(programCounter) == null) return false;
    return instructions.get(programCounter).equals(RESERVED);
  }

  /**
   * Returns if the ProgramCounter is writable.
   *
   * <p>The ProgramCounter is not writable if there is already an instruction located at that
   * ProgramCounter or if the reserved ProgramCounters forbid usage. Thus the answer may depend on
   * the setting of {@link #allowReserved(boolean)}.
   *
   * @param programCounter the ProgramCounter in question
   * @return true if you may write to the location
   * @throws IllegalArgumentException if the ProgramCounter is negative
   */
  public boolean isFree(int programCounter) {
    if (programCounter < 0) {
      throw new IllegalArgumentException("The Program-Counter may not be negative");
    }

    if (instructions.size() - 1 < programCounter) return true;
    if (instructions.get(programCounter) == null) return true;
    return false;
  }

  /**
   * Not exactly dual to {@link #isFree(int)}.
   *
   * <p>Returns whether an instruction is located at ProgramCounter. Thus it is a shortcut to {@link
   * #getStatementAt(int)} != null.
   *
   * <p>It is not the exact dual to {@link #isFree(int)} as it does not consider reserved
   * ProgramCounters.
   *
   * @param programCounter the ProgramCounter in question
   * @return true if there's an instruction at that program counter
   * @throws IllegalArgumentException if the ProgramCounter is negative
   */
  public boolean isUsed(int programCounter) {
    if (programCounter < 0) {
      throw new IllegalArgumentException("The Program-Counter may not be negative");
    }

    if (instructions.size() - 1 < programCounter) return false;
    if (instructions.get(programCounter) == null) return false;
    if (instructions.get(programCounter).equals(RESERVED)) return false;
    return true;
  }

  /**
   * Like {@link #addStatement(SSAInstruction)} but may replace an existing one.
   *
   * @param statement The statement to add without care of overwriting
   * @return true if a statement has actually been overwritten
   * @throws IllegalStateException if you may not write to the ProgramCounter due to the setting of
   *     {@link #allowReserved(boolean)} or {@link #getMethodSummary()} has been called and thus
   *     this summary got locked.
   * @throws NullPointerException if statement is null
   * @throws IllegalArgumentException if the statement has set an invalid ProgramCounter
   */
  public boolean overwriteStatement(SSAInstruction statement) {
    if (this.locked) {
      throw new IllegalStateException("Summary locked due to call to getMethodSummary().");
    }
    if (statement == null) {
      throw new NullPointerException("Statement is null!");
    }
    if (statement.iIndex() < 0) {
      throw new IllegalArgumentException("Statement has a negative iindex");
    }
    if (!this.allowReservedPC && isReserved(statement.iIndex())) {
      throw new IllegalStateException(
          "ProgramCounter " + statement.iIndex() + " is reserved! Use allowReserved(true).");
    }
    if (statement.iIndex() > this.currentProgramCounter) {
      throw new IllegalArgumentException(
          "IIndex "
              + statement.iIndex()
              + " is greater than currentProgramCounter. Use getNextProgramCounter.");
    }

    boolean didOverwrite = isUsed(statement.iIndex());
    while (this.instructions.size() - 1 < statement.iIndex()) this.instructions.add(null);
    if (DEBUG) {
      System.err.printf("Setting %s to %s\n", statement.iIndex(), statement);
    }
    this.instructions.set(statement.iIndex(), statement);
    return didOverwrite;
  }

  /**
   * Generates the MethodSummary and locks class.
   *
   * @throws IllegalStateException if you altered the referenced (by constructor) summary
   * @return the finished MethodSummary
   */
  public MethodSummary getMethodSummary() {
    if (locked) {
      // Already generated
      return this.summary;
    }
    if (summary.getNumberOfStatements() > 0) {
      throw new IllegalStateException(
          "Meanwhile Statements have been added to the summary given "
              + "to the constructor. This behavior is not supported!");
    }
    this.locked = true;
    for (int i = 0; i < this.instructions.size(); ++i) {
      final SSAInstruction inst = this.instructions.get(i);
      if (inst == null) {
        if (DEBUG) {
          System.err.printf("No instruction at iindex %d\n", i);
        }
        this.summary.addStatement(null);
      } else if (inst == RESERVED) {
        // replace reserved slots by 'goto next' statements
        this.summary.addStatement(new SSAGotoInstruction(i, i + 1));
      } else {
        if (DEBUG) {
          System.err.printf("Adding @%s: ", inst);
        }
        this.summary.addStatement(inst);
      }
    }

    // Let the GC free instructions..
    this.instructions = null;

    return this.summary;
  }

  // /**
  // *  Re-enable write access to VolatileMethodSummary (CAUTION...).
  // *
  // *  On a call to {@link #getMethodSummary()} the AndroidModelMethodSummary gets locked
  // *  to prevent unintended behaviour.
  // *
  // *  Through the call of this function you gain back write access. However you should
  // *  know what you are doing as the "exported" MethodSummary will not get updated. A
  // *  AndroidModelMethodSummary of course starts in unlocked state.
  // */
  // public void unlock() {
  //    this.locked = false;
  // }

  //
  // Now for the stuff you should be familiar with from MethodSummary, but with a view
  // more checks
  //

  /**
   * Adds a statement to the MethodSummary.
   *
   * @param statement The statement to be added
   * @throws IllegalStateException if you may not write to the ProgramCounter due to the setting of
   *     {@link #allowReserved(boolean)} or {@link #getMethodSummary()} has been called and thus
   *     this summary got locked.
   * @throws NullPointerException if statement is null
   * @throws IllegalArgumentException if the statement has set an invalid ProgramCounter or if there
   *     is already a statement at the statements iindex. In this case you can use {@link
   *     #overwriteStatement(SSAInstruction)}.
   */
  public void addStatement(SSAInstruction statement) {
    if (isUsed(statement.iIndex())) {
      throw new IllegalArgumentException(
          "ProgramCounter "
              + statement.iIndex()
              + " is in use! By "
              + getStatementAt(statement.iIndex())
              + " Use overwriteStatement().");
    }

    overwriteStatement(statement);
  }

  /** Optionally add a name for a local variable. */
  public void setLocalName(final int number, final String name) {
    localNames.put(number, Atom.findOrCreateAsciiAtom(name));
  }

  /**
   * Set localNames merges with existing names.
   *
   * <p>If a key in merge exists the value is overwritten if not the value is kept (it's a putAll on
   * the internal map).
   */
  public void setLocalNames(Map<Integer, Atom> merge) {
    localNames.putAll(merge);
  }

  /** A mapping from SSA-Values to Variable-names. */
  public Map<Integer, Atom> getLocalNames() {
    return localNames;
  }

  /**
   * Assigns a new Constant to a SSA-Value.
   *
   * @throws IllegalStateException if you redefine a constant or use the number of an existent
   *     SSA-Variable
   * @throws IllegalArgumentException if value is null or negative
   */
  public void addConstant(java.lang.Integer vn, ConstantValue value) {

    if ((summary.getConstants() != null) && summary.getConstants().containsKey(vn)) {
      throw new IllegalStateException("You redefined a constant at number " + vn);
    }
    if (vn <= 0) {
      throw new IllegalArgumentException("SSA-Value may not be zero or negative.");
    }
    this.summary.addConstant(vn, value);
  }

  /**
   * Adds poison to the function.
   *
   * <p>This call gets passed directly to the internal MethodSummary.
   */
  public void addPoison(java.lang.String reason) {
    this.summary.addPoison(reason);
  }

  /**
   * Retrieves a mapping from SSA-Number to a constant.
   *
   * <p>You can add Constants using the function {@link #addConstant(java.lang.Integer,
   * ConstantValue)}. A call to this function gets passed directly to the internal MethodSummary.
   *
   * @return a mapping from SSA-Number to assigned ConstantValue
   */
  public java.util.Map<java.lang.Integer, ConstantValue> getConstants() {
    return this.summary.getConstants();
  }

  /**
   * Retrieve the Method this Summary implements.
   *
   * <p>You'll get a MemberReference which contains the declaring class (which should be the
   * FakeRootClass in most cases) and the signature of the method.
   *
   * <p>This call gets passed directly to the internal MethodSummary.
   *
   * @return the implemented method as stated above
   */
  public MemberReference getMethod() {
    return this.summary.getMethod();
  }

  /**
   * Gets you a non-reserved ProgramCounter you can write to.
   *
   * <p>This function returns the next ProgramCounter for which not({@link #isUsed(int)}) holds.
   * Thus it will _not_ give you a ProgramCounter which is reserved even if you enabled writing to
   * reserved ProgramCounters using {@link #allowReserved(boolean)}! You'll have to keep track of
   * them on your own.
   *
   * @return A non-reserved writable ProgramCounter
   */
  public int getNextProgramCounter() {
    while (isUsed(this.currentProgramCounter) || isReserved(this.currentProgramCounter)) {
      this.currentProgramCounter++;
    }
    while (this.instructions.size() < this.currentProgramCounter) this.instructions.add(null);
    return this.currentProgramCounter;
  }

  /**
   * Get the count of parameters of the Method this Summary implements.
   *
   * <p>This call gets passed directly to the internal MethodSummary.
   *
   * @return Number of parameters
   */
  public int getNumberOfParameters() {
    return this.summary.getNumberOfParameters();
  }

  /**
   * Gets you the TypeReference of a parameter.
   *
   * <p>This call gets passed directly to the internal MethodSummary after some checks.
   *
   * @return the TypeReference of the i-th parameter.
   * @throws IllegalArgumentException if the parameter is zero or negative
   * @throws ArrayIndexOutOfBoundsException if the parameter is to large
   */
  public TypeReference getParameterType(int i) {
    if (i <= 0) {
      throw new IllegalArgumentException(
          "The parameter number may not be zero or negative! " + i + " given");
    }
    if (i >= this.summary.getNumberOfParameters()) {
      throw new ArrayIndexOutOfBoundsException("No such parameter index: " + i);
    }

    return this.summary.getParameterType(i);
  }

  /**
   * Retrieves the poison set using {@link #addPoison(java.lang.String)}
   *
   * @return The poison-String
   */
  public java.lang.String getPoison() {
    return this.summary.getPoison();
  }

  /**
   * Retrieves the value of Poison-Level.
   *
   * <p>This call gets passed directly to the internal MethodSummary.
   *
   * @return the poison level
   */
  public byte getPoisonLevel() {
    return this.summary.getPoisonLevel();
  }

  /**
   * Retrieves the return-type of the Function whose body this Summary implements.
   *
   * <p>This call gets passed directly to the internal MethodSummary.
   */
  public TypeReference getReturnType() {
    return this.summary.getReturnType();
  }

  /**
   * Get all statements added to the Summary.
   *
   * <p>This builds a copy of the internal list and may contain 'null'-values if no instruction has
   * been placed at a particular pc.
   *
   * @return The statements of the summary
   */
  public SSAInstruction[] getStatements() {
    SSAInstruction[] ret = new SSAInstruction[this.instructions.size()];
    ret = this.instructions.toArray(ret);

    // Remove Reserved
    for (int i = 0; i < ret.length; ++i) {
      if (ret[i].equals(RESERVED)) {
        ret[i] = null;
      }
    }
    return ret;
  }

  /**
   * Returns if Poison has been added using {@link #addPoison(java.lang.String)}.
   *
   * <p>This call gets passed directly to the internal MethodSummary.
   *
   * @return true if poison has been added
   */
  public boolean hasPoison() {
    return this.summary.hasPoison();
  }

  /**
   * Returns if the implemented method is a factory.
   *
   * <p>This call gets passed directly to the internal MethodSummary.
   *
   * @return true if it's a factory
   */
  public boolean isFactory() {
    return this.summary.isFactory();
  }

  /**
   * Return if the implemented method is a native one (which it shouldn't be).
   *
   * <p>This call gets passed directly to the internal MethodSummary.
   *
   * @return almost always false
   */
  public boolean isNative() {
    return this.summary.isNative();
  }

  /**
   * Return if the implemented method is static.
   *
   * <p>A static method may not access non-static (and thus instance-specific) content.
   *
   * @return true if the method is static.
   */
  public boolean isStatic() {
    return this.summary.isStatic();
  }

  /**
   * Set the value returned by {@link #isFactory()}
   *
   * @throws IllegalStateException if summary was locked
   */
  public void setFactory(boolean b) {
    if (this.locked) {
      throw new IllegalStateException("Summary is locked. Unlock using unlock()");
    }
    this.summary.setFactory(b);
  }

  /**
   * Set the value returned by {@link #getPoisonLevel()}
   *
   * @throws IllegalStateException if summary was locked
   */
  public void setPoisonLevel(byte b) {
    if (this.locked) {
      throw new IllegalStateException("Summary is locked. Unlock using unlock()");
    }
    this.summary.setPoisonLevel(b);
  }

  /**
   * Set the value returned by {@link #isStatic()}
   *
   * @throws IllegalStateException if summary was locked
   */
  public void setStatic(boolean b) {
    if (this.locked) {
      throw new IllegalStateException("Summary is locked. Unlock using unlock()");
    }
    this.summary.setStatic(b);
  }

  /** Generates a String-Representation of an instance of the class. */
  @Override
  public java.lang.String toString() {
    return "VolatileMethodSummary of " + this.summary.toString();
  }
}
